#include "PrecompiledHeaderRenderer_OpenGL.h"
#include "ShaderWrapper.h"

/*--------------------------------------------------------------------------*/
std::map<std::string, GLShader*> ShaderWrapper::_shadersMap;
std::map<std::string, GLProgram*> ShaderWrapper::_programsMap;
GLProgram* ShaderWrapper::_boundProgram = NULL;
/*--------------------------------------------------------------------------*/

ShaderWrapper::ShaderWrapper()
{
}

/*--------------------------------------------------------------------------*/

ShaderWrapper::~ShaderWrapper()
{
	for (auto i = _shadersMap.begin(); i != _shadersMap.end(); ++i)
	{
		AssertLogE(true, "Shader %s not deleted!", i->first.c_str());
	}

	for (auto i = _programsMap.begin(); i != _programsMap.end(); ++i)
	{
		AssertLogE(true, "Program %s not deleted!", i->first.c_str());
	}

	AssertLogC(_shadersMap.size() != 0, "Some shaders left undeleted!");
	AssertLogC(_programsMap.size() != 0, "Some programs left undeleted!");
}

/*--------------------------------------------------------------------------*/

GLShader* ShaderWrapper::GenShader(const std::string& name, GLenum type, const std::string& source)
{
	AssertLogC(name == "", "Zero length name");
	AssertLogC(_shadersMap[name] != NULL, "Shader %s already exists!", name.c_str());

	GLShader* s = new GLShader(name);
	s->handle = glCreateShader(type);

	AssertLogC(s->handle == 0, "Failed to create shader %s", name.c_str());
	_shadersMap[name] = s;

	s->references = 0;
	s->source = source;
	s->type = type;

	return s;
}

/*---------------------------------------------------------------------------*/

void ShaderWrapper::DeleteShader(GLShader** shader)
{
	if (shader && *shader)
	{
		AssertLogC((*shader)->references != 0, "Some references to %s left", (*shader)->name.c_str());
		AssertLogC((*shader)->handle == 0, "Deleting 0 shader program.");

		/*--------------------------------------------------------------------------*/

		auto i = _shadersMap.find((*shader)->name);
		AssertLogC(i == _shadersMap.end(), "Shader %s not exists!", (*shader)->name.c_str());
		_shadersMap.erase(i);

		/*--------------------------------------------------------------------------*/

		glDeleteShader((*shader)->handle);
		delete *shader;
		*shader = NULL;
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::UnbindProgram()
{
	glUseProgram(0);
	_boundProgram = NULL;
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::BindProgram(GLProgram* shader)
{
	_boundProgram = shader;

	if (shader != NULL)
	{
		glUseProgram(shader->handle);
	}
	else
	{
		glUseProgram(0);
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::UploadUboData()
{
	if (_boundProgram == NULL)
	{
		return;
	}

	AssertLogC(_boundProgram->handle == 0, "Bound shader handle is 0");

	uint totalshaders = _boundProgram->attachedShaders.size();

	for (uint i = 0; i < totalshaders; ++i)
	{
		uint ubos = _boundProgram->attachedShaders[i]->data_ubo.size();

		for (uint j = 0; j < ubos; ++j)
		{
			ROpenGL::UpdateUniformBuffer(_boundProgram->attachedShaders[i]->data_ubo[j].ubo);
		}
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::UploadSamplerData()
{
	if (_boundProgram == NULL)
	{
		return;
	}

	/*----Uniforms (samplers)---------------------------------------------------*/

	uint target = 10;
	GLShader* shaderPtn = NULL;

	for (uint i = 0; i < _boundProgram->num_attachedShaders; ++i)
	{
		shaderPtn = _boundProgram->attachedShaders[i];

		if (shaderPtn->samplersBound)
		{
			for (uint j = 0; j < shaderPtn->num_samplers; ++j)
			{
				GLShader::Sampler& sampler = shaderPtn->data_samplers[j];

				AssertLogC(sampler.textureCtn == NULL, "NULL pointer as Sampler!");
				AssertLogC(sampler.location == 0, "Location for sampler %s is 0", sampler.name.c_str());

				ROpenGL::BindTextureAt(target, *sampler.textureCtn, true);
				glUniform1i(sampler.location, target);
				++target;
			}
		}
		else
		{
			bool allSamplersBoundTest = true;

			for (uint j = 0; j < shaderPtn->num_samplers; ++j)
			{
				GLShader::Sampler& sampler = shaderPtn->data_samplers[j];

				if (sampler.textureCtn == NULL || *sampler.textureCtn == NULL)
				{
					allSamplersBoundTest = false;
					sampler.textureCtn = ROpenGL::GetSampler(sampler.name);
				}

				if (sampler.textureCtn != NULL && *sampler.textureCtn != NULL)
				{
					ROpenGL::BindTextureAt(target, *sampler.textureCtn, true);
					glUniform1i(sampler.location, target);
					++target;
				}
				else
				{
					AssertLogE(allSamplersBoundTest == false, "Shader %s has uninitialized sampler: %s", shaderPtn->name.c_str(), sampler.name.c_str());
				}
			}

			shaderPtn->samplersBound = allSamplersBoundTest;
		}
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::BindUBOToShader(GLShader* shader, GLUniformBuffer* source)
{
	AssertLogC(shader == NULL, "Shader is NULL!");
	AssertLogC(source == NULL, "NULL as Uniform Buffer");

	GLShader::UBO ubo;
	ubo.ubo = source;

	shader->data_ubo.push_back(ubo);
	shader->dirty = true;
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::BindInputToShader(GLShader* shader, const std::string& type, const std::string& name, int location)
{
	AssertLogC(shader == NULL, "Shader is NULL!");

	if (name == "FONT_AUTO")
	{
		BindInputToShader(shader, "vec2", "in_LetterTexcoord", location + 0);
		BindInputToShader(shader, "vec2", "in_LetterSize", location + 1);
		BindInputToShader(shader, "vec2", "in_LetterPosition", location + 2);
	}
	else
	{
		GLShader::Data data;
		data.name = name;
		data.type = type;
		data.location = location;

		shader->data_inputs.push_back(data);
		shader->dirty = true;
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::BindOutputToShader(GLShader* shader, const std::string& type, const std::string& name, int location)
{
	AssertLogC(shader == NULL, "Shader is NULL!");

	GLShader::Data data;
	data.name = name;
	data.type = type;
	data.location = location;

	shader->data_outputs.push_back(data);
	shader->dirty = true;
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::BindDefineToShader(GLShader* shader, const std::string& name, const std::string& value)
{
	AssertLogC(shader == NULL, "Shader is NULL!");

	GLShader::Macro macro;
	macro.name = name;
	macro.value = value;

	shader->data_macros.push_back(macro);
	shader->dirty = true;
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::BindSamplerToShader(GLShader* shader, const std::string& name)
{
	AssertLogC(shader == NULL, "Shader is NULL!");

	GLShader::Sampler sampler;
	sampler.name = name;
	sampler.textureCtn = ROpenGL::GetSampler(name);

	shader->data_samplers.push_back(sampler);
	shader->dirty = true;
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::Compile(GLShader* shader)
{
	AssertLogC(shader == NULL, "Shader is NULL!");

	/*--------------------------------------------------------------------------*/

	std::stringstream ubos;
	uint ubosCount = shader->data_ubo.size();

	for (uint i = 0; i < ubosCount; ++i)
	{
		ubos << shader->data_ubo[i].ubo->glslContent << "\n";
	}

	std::string ubosContent = ubos.str();

	/*--------------------------------------------------------------------------*/

	std::stringstream defines;
	uint definesCount = shader->data_macros.size();

	for (uint i = 0; i < definesCount; ++i)
	{
		defines << "#define " << shader->data_macros[i].name << " " << shader->data_macros[i].value << "\n";
	}

	std::string definesContent = defines.str();

	/*--------------------------------------------------------------------------*/

	std::stringstream samplers;
	uint samplersCount = shader->data_samplers.size();

	for (uint i = 0; i != samplersCount; ++i)
	{
		samplers << "uniform sampler2D " << shader->data_samplers[i].name << ";\n";
	}

	std::string samplersContent = samplers.str();

	/*--------------------------------------------------------------------------*/

	std::stringstream inputs;
	uint inputsCount = shader->data_inputs.size();

	for (uint i = 0; i < inputsCount; ++i)
	{
		if (shader->data_inputs[i].location != -1)
		{
			inputs << "layout(location = " << shader->data_inputs[i].location << ") ";
		}

		inputs << "in " << shader->data_inputs[i].type << " " << shader->data_inputs[i].name << ";\n";
	}

	std::string inputsContent = inputs.str();

	/*--------------------------------------------------------------------------*/

	std::stringstream outputs;
	uint outputsCount = shader->data_outputs.size();

	for (uint i = 0; i < outputsCount; ++i)
	{
		if (shader->data_outputs[i].location != -1)
		{
			outputs << "layout(location = " << shader->data_outputs[i].location << ") ";
		}

		outputs << "out " << shader->data_outputs[i].type << " " << shader->data_outputs[i].name << ";\n";
	}

	std::string outputsContent = outputs.str();

	/*--------------------------------------------------------------------------*/

	std::stringstream ss;

	ss << "// CREATED WITH: " << shader->name << "\n";
	ss << "#version 330\n";
	ss << "#extension GL_ARB_explicit_uniform_location : require\n\n";
	ss << "//ViaMain: Defines:\n" << definesContent << "\n";
	ss << "//ViaMain: Uniform Buffer Objects:\n" << ubosContent << "\n";
	ss << "//ViaMain: Samplers:\n" << samplersContent << "\n";
	ss << "//ViaMain: Inputs:\n" << inputsContent << "\n";
	ss << "//ViaMain: Outputs:\n" << outputsContent << "\n";
	ss << "//ViaMain: Shader code:\n" << shader->source << "\n";
	ss << "//ViaMain: EndOfFile\n";

	shader->source = ss.str();
	const char* ccptn = shader->source.c_str();

	int success = 0;

	glShaderSource(shader->handle, 1, &ccptn, 0);
	glCompileShader(shader->handle);
	glGetShaderiv(shader->handle, GL_COMPILE_STATUS, &success);

	if (success == GL_FALSE)
	{
		int infoLength = 0;
		glGetShaderiv(shader->handle, GL_INFO_LOG_LENGTH, &infoLength);

		char* log = new char[infoLength];
		glGetShaderInfoLog(shader->handle, infoLength, &success, log);

		AssertLogC(true, "In shader %s:\n%s", shader->name.c_str(), log);

		shader->compilationLog = log;
		delete [] log;
	}

#ifdef RENDERER_WRITE_LOGS
	std::ofstream file("../Logs/SHADER_" + shader->name + ".log");
	file << *shader;
	file.close();
#endif
}

/*--------------------------------------------------------------------------*/

GLShader* ShaderWrapper::GetShader(const std::string& name)
{
	auto iter = _shadersMap.find(name);

	if (iter == _shadersMap.end())
	{
		return NULL;
	}
	else
	{
		return iter->second;
	}
}

/*--------------------------------------------------------------------------*/

GLProgram* ShaderWrapper::GenProgram(const std::string& name)
{
	AssertLogC(name == "", "Zero length name");
	AssertLogC(_programsMap[name] != NULL, "Program %s already exists!", name.c_str());

	GLProgram* s = new GLProgram(name);
	s->handle = glCreateProgram();

	AssertLogC(s->handle == 0, "Failed to create program %s", name.c_str());

	_programsMap[name] = s;

	return s;

}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::DeleteProgram(GLProgram** program)
{
	if (program && *program)
	{
		AssertLogC((*program)->handle == 0, "Deleting 0 program.");

		/*--------------------------------------------------------------------------*/

		auto i = _programsMap.find((*program)->name);
		AssertLogC(i == _programsMap.end(), "Program %s not exists!", (*program)->name.c_str());
		_programsMap.erase(i);

		/*--------------------------------------------------------------------------*/

		while ((*program)->attachedShaders.empty() == false)
		{
			GLShader* shader = (*program)->attachedShaders.front();

			DetachShader(*program, shader);
			DeleteShader(&shader);
		}

		glDeleteProgram((*program)->handle);
		delete *program;
		*program = NULL;
	}
}

/*--------------------------------------------------------------------------*/

GLProgram* ShaderWrapper::GetProgram(const std::string& name)
{
	auto iter = _programsMap.find(name);

	if (iter == _programsMap.end())
	{
		return NULL;
	}
	else
	{
		return iter->second;
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::Link(GLProgram* program)
{
	if (program->linked)
	{
		return;
	}

	AssertLogC(glIsProgram(program->handle) == false, "%d is not a program!", program->name);
	glLinkProgram(program->handle);

	int IsLinked = 0;
	glGetProgramiv(program->handle, GL_LINK_STATUS, (int*) &IsLinked);

	program->linked = (bool) IsLinked;

	if (IsLinked == GL_FALSE)
	{
		int maxLength = 0;
		glGetProgramiv(program->handle, GL_INFO_LOG_LENGTH, &maxLength);

		char* log = new char[maxLength];
		glGetProgramInfoLog(program->handle, maxLength, &maxLength, log);

		AssertLogC(true, "In shader %s:\n%s", program->name.c_str(), log);
		program->linkLog = log;
		delete [] log;
	}

	/*--------outputy z shadera-------------------------------------------------*/
	uint outputsCount = 0;

	for (auto output = program->attachedShaders.back()->data_outputs.begin(); output != program->attachedShaders.back()->data_outputs.end(); ++output)
	{
		++outputsCount;
		glBindFragDataLocation(program->handle, output->location, output->name.c_str());
	}

	AssertLogC(outputsCount > GL_MAX_DRAW_BUFFERS, "Too many outputs in %s.", program->name.c_str());

	/*---Zapelnianie lokacjami z shadera--------------------------------------*/

	for (auto sh = program->attachedShaders.begin(); sh != program->attachedShaders.end(); ++sh)
	{
		for (auto i = (*sh)->data_ubo.begin(); i != (*sh)->data_ubo.end();)
		{
			i->blockIndex = glGetUniformBlockIndex(program->handle, i->ubo->name.c_str());

			if (i->blockIndex == GL_INVALID_INDEX)
			{
				AssertLogC(true, "UBO %s not used/found in %s", i->ubo->name.c_str(), (*sh)->name.c_str());
				i = (*sh)->data_ubo.erase(i);
			}
			else
			{
				glUniformBlockBinding(program->handle, i->blockIndex, i->ubo->bindingIndex);
				++i;
			}
		}

		/*--------------------------------------------------------------------------*/

		for (auto i = (*sh)->data_samplers.begin(); i != (*sh)->data_samplers.end(); ++i)
		{
			i->location = glGetUniformLocation(program->handle, i->name.c_str());
			AssertLogC(i->location == -1, "Sampler %s not used/found in %s", i->name.c_str(), (*sh)->name.c_str());
		}

		(*sh)->samplersBound = false;

		OptimizeShader(*sh);
	}

#ifdef RENDERER_WRITE_LOGS
	std::ofstream file("../Logs/PROGRAM_" + program->name + ".log");
	file << *program;
	file.close();
#endif
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::AttachShader(GLProgram* program, GLShader* shader)
{
	if (program && shader)
	{
#ifdef _DEBUG
		uint size = program->attachedShaders.size();

		for (uint i = 0; i < size; ++i)
		{
			AssertLogC(program->attachedShaders[i]->type == shader->type, "Trying to duplicate shader type in %s nad %s!", program->name.c_str(), shader->name.c_str());
		}

#endif
		shader->references++;

		program->attachedShaders.push_back(shader);
		glAttachShader(program->handle, shader->handle);

		program->num_attachedShaders = program->attachedShaders.size();
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::DetachShader(GLProgram* program, GLShader* shader)
{
	if (program && shader)
	{
		uint size = program->attachedShaders.size();

		for (auto iter = program->attachedShaders.begin(); iter != program->attachedShaders.end();)
		{
			if (*iter == shader)
			{
				(*iter)->references--;
				iter = program->attachedShaders.erase(iter);
				glDetachShader(program->handle, shader->handle);
			}
			else
			{
				iter++;
			}
		}
	}
}

/*--------------------------------------------------------------------------*/

void ShaderWrapper::OptimizeShader(GLShader* shader)
{
	if (shader && shader->dirty)
	{
		shader->dirty = false;

		shader->num_inputs		= shader->data_inputs.size();
		shader->num_macros		= shader->data_macros.size();
		shader->num_outputs		= shader->data_outputs.size();
		shader->num_samplers	= shader->data_samplers.size();
		shader->num_ubo			= shader->data_ubo.size();
	}
}

/*--------------------------------------------------------------------------*/
